/*
 * Copyright (c) 2020 - present, Inspur Genersoft Co., Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.iec.edp.caf.session.filter;

import io.iec.edp.caf.session.holder.CafSecurityAPIKeyHolder;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.session.web.http.*;

import java.io.IOException;
import java.time.Instant;
import java.util.Iterator;
import java.util.List;
import javax.servlet.FilterChain;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.springframework.core.annotation.Order;


@Order(-2147483598)
public class CafSessionRepositoryFilter<S extends Session> extends CafOncePerRequestFilter {
    private static final String SESSION_LOGGER_NAME = SessionRepositoryFilter.class.getName().concat(".SESSION_LOGGER");
    private static final Log SESSION_LOGGER;
    public static final String SESSION_REPOSITORY_ATTR;
    public static final String INVALID_SESSION_ID_ATTR;
    private static final String CURRENT_SESSION_ATTR;
    public static final int DEFAULT_ORDER = -2147483598;
    private final SessionRepository<S> sessionRepository;
    private HttpSessionIdResolver httpSessionIdResolver = new CookieHttpSessionIdResolver();

    public CafSessionRepositoryFilter(SessionRepository<S> sessionRepository) {
        if (sessionRepository == null) {
            throw new IllegalArgumentException("sessionRepository cannot be null");
        } else {
            this.sessionRepository = sessionRepository;
        }
    }

    public void setHttpSessionIdResolver(HttpSessionIdResolver httpSessionIdResolver) {
        if (httpSessionIdResolver == null) {
            throw new IllegalArgumentException("httpSessionIdResolver cannot be null");
        } else {
            this.httpSessionIdResolver = httpSessionIdResolver;
        }
    }

    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
        CafSessionRepositoryFilter<S>.SessionRepositoryRequestWrapper wrappedRequest = new CafSessionRepositoryFilter.SessionRepositoryRequestWrapper(request, response);
        CafSessionRepositoryFilter.SessionRepositoryResponseWrapper wrappedResponse = new CafSessionRepositoryFilter.SessionRepositoryResponseWrapper(wrappedRequest, response);

        try {
            filterChain.doFilter(wrappedRequest, wrappedResponse);
        } finally {
            wrappedRequest.commitSession();

            //移除caf API key场景赋值
            CafSecurityAPIKeyHolder.remove();
        }

    }

    protected void doFilterNestedErrorDispatch(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        this.doFilterInternal(request, response, filterChain);
    }

    static {
        SESSION_LOGGER = LogFactory.getLog(SESSION_LOGGER_NAME);
        SESSION_REPOSITORY_ATTR = SessionRepository.class.getName();
        INVALID_SESSION_ID_ATTR = SESSION_REPOSITORY_ATTR + ".invalidSessionId";
        CURRENT_SESSION_ATTR = SESSION_REPOSITORY_ATTR + ".CURRENT_SESSION";
    }

    private final class SessionRepositoryRequestWrapper extends HttpServletRequestWrapper {
        private final HttpServletResponse response;
        private S requestedSession;
        private boolean requestedSessionCached;
        private String requestedSessionId;
        private Boolean requestedSessionIdValid;
        private boolean requestedSessionInvalidated;

        private SessionRepositoryRequestWrapper(HttpServletRequest request, HttpServletResponse response) {
            super(request);
            this.response = response;
        }

        private void commitSession() {
            CafSessionRepositoryFilter<S>.SessionRepositoryRequestWrapper.HttpSessionWrapper wrappedSession = this.getCurrentSession();
            if (wrappedSession == null) {
                if (this.isInvalidateClientSession()) {
                    CafSessionRepositoryFilter.this.httpSessionIdResolver.expireSession(this, this.response);
                }
            } else {
                S session = wrappedSession.getSession();
                this.clearRequestedSessionCache();
                CafSessionRepositoryFilter.this.sessionRepository.save(session);
                String sessionId = session.getId();
                if (!this.isRequestedSessionIdValid() || !sessionId.equals(this.getRequestedSessionId())) {
                    CafSessionRepositoryFilter.this.httpSessionIdResolver.setSessionId(this, this.response, sessionId);
                }
            }

        }

        private CafSessionRepositoryFilter<S>.SessionRepositoryRequestWrapper.HttpSessionWrapper getCurrentSession() {
            return (CafSessionRepositoryFilter.SessionRepositoryRequestWrapper.HttpSessionWrapper) this.getAttribute(CafSessionRepositoryFilter.CURRENT_SESSION_ATTR);
        }

        private void setCurrentSession(CafSessionRepositoryFilter<S>.SessionRepositoryRequestWrapper.HttpSessionWrapper currentSession) {
            if (currentSession == null) {
                this.removeAttribute(CafSessionRepositoryFilter.CURRENT_SESSION_ATTR);
            } else {
                this.setAttribute(CafSessionRepositoryFilter.CURRENT_SESSION_ATTR, currentSession);
            }

        }

        public String changeSessionId() {
            HttpSession session = this.getSession(false);
            if (session == null) {
                throw new IllegalStateException("Cannot change session ID. There is no session associated with this request.");
            } else {
                return this.getCurrentSession().getSession().changeSessionId();
            }
        }

        public boolean isRequestedSessionIdValid() {
            if (this.requestedSessionIdValid == null) {
                S requestedSession = this.getRequestedSession();
                if (requestedSession != null) {
                    requestedSession.setLastAccessedTime(Instant.now());
                }

                return this.isRequestedSessionIdValid(requestedSession);
            } else {
                return this.requestedSessionIdValid;
            }
        }

        private boolean isRequestedSessionIdValid(S session) {
            if (this.requestedSessionIdValid == null) {
                this.requestedSessionIdValid = session != null;
            }

            return this.requestedSessionIdValid;
        }

        private boolean isInvalidateClientSession() {
            return this.getCurrentSession() == null && this.requestedSessionInvalidated;
        }

        public CafSessionRepositoryFilter<S>.SessionRepositoryRequestWrapper.HttpSessionWrapper getSession(boolean create) {
            CafSessionRepositoryFilter<S>.SessionRepositoryRequestWrapper.HttpSessionWrapper currentSession = this.getCurrentSession();
            if (currentSession != null) {
                return currentSession;
            } else {
                S requestedSession = this.getRequestedSession();
                if (requestedSession != null) {
                    if (this.getAttribute(CafSessionRepositoryFilter.INVALID_SESSION_ID_ATTR) == null) {
                        requestedSession.setLastAccessedTime(Instant.now());
                        this.requestedSessionIdValid = true;
                        currentSession = new CafSessionRepositoryFilter.SessionRepositoryRequestWrapper.HttpSessionWrapper(requestedSession, this.getServletContext());
                        currentSession.markNotNew();
                        this.setCurrentSession(currentSession);
                        return currentSession;
                    }
                } else {
                    if (CafSessionRepositoryFilter.SESSION_LOGGER.isDebugEnabled()) {
                        CafSessionRepositoryFilter.SESSION_LOGGER.debug("No session found by id: Caching result for getSession(false) for this HttpServletRequest.");
                    }

                    this.setAttribute(CafSessionRepositoryFilter.INVALID_SESSION_ID_ATTR, "true");
                }

                if (!create) {
                    return null;
                } else if (CafSessionRepositoryFilter.this.httpSessionIdResolver instanceof CookieHttpSessionIdResolver && this.response.isCommitted()) {
                    throw new IllegalStateException("Cannot create a session after the response has been committed");
                } else {
                    if (CafSessionRepositoryFilter.SESSION_LOGGER.isDebugEnabled()) {
                        CafSessionRepositoryFilter.SESSION_LOGGER.debug("A new session was created. To help you troubleshoot where the session was created we provided a StackTrace (this is not an error). You can prevent this from appearing by disabling DEBUG logging for " + CafSessionRepositoryFilter.SESSION_LOGGER_NAME, new RuntimeException("For debugging purposes only (not an error)"));
                    }

                    S session = CafSessionRepositoryFilter.this.sessionRepository.createSession();
                    session.setLastAccessedTime(Instant.now());
                    currentSession = new CafSessionRepositoryFilter.SessionRepositoryRequestWrapper.HttpSessionWrapper(session, this.getServletContext());
                    this.setCurrentSession(currentSession);
                    return currentSession;
                }
            }
        }

        public CafSessionRepositoryFilter<S>.SessionRepositoryRequestWrapper.HttpSessionWrapper getSession() {
            return this.getSession(true);
        }

        public String getRequestedSessionId() {
            if (this.requestedSessionId == null) {
                this.getRequestedSession();
            }

            return this.requestedSessionId;
        }

        public RequestDispatcher getRequestDispatcher(String path) {
            RequestDispatcher requestDispatcher = super.getRequestDispatcher(path);
            return new CafSessionRepositoryFilter.SessionRepositoryRequestWrapper.SessionCommittingRequestDispatcher(requestDispatcher);
        }

        private S getRequestedSession() {
            if (!this.requestedSessionCached) {
                List<String> sessionIds = CafSessionRepositoryFilter.this.httpSessionIdResolver.resolveSessionIds(this);
                Iterator var2 = sessionIds.iterator();

                while (var2.hasNext()) {
                    String sessionId = (String) var2.next();
                    if (this.requestedSessionId == null) {
                        this.requestedSessionId = sessionId;
                    }

                    S session = CafSessionRepositoryFilter.this.sessionRepository.findById(sessionId);
                    if (session != null) {
                        this.requestedSession = session;
                        this.requestedSessionId = sessionId;
                        break;
                    }
                }

                this.requestedSessionCached = true;
            }

            return this.requestedSession;
        }

        private void clearRequestedSessionCache() {
            this.requestedSessionCached = false;
            this.requestedSession = null;
            this.requestedSessionId = null;
        }

        private final class SessionCommittingRequestDispatcher implements RequestDispatcher {
            private final RequestDispatcher delegate;

            SessionCommittingRequestDispatcher(RequestDispatcher delegate) {
                this.delegate = delegate;
            }

            public void forward(ServletRequest request, ServletResponse response) throws ServletException, IOException {
                this.delegate.forward(request, response);
            }

            public void include(ServletRequest request, ServletResponse response) throws ServletException, IOException {
                CafSessionRepositoryFilter.SessionRepositoryRequestWrapper.this.commitSession();
                this.delegate.include(request, response);
            }
        }

        private final class HttpSessionWrapper extends CafHttpSessionAdapter<S> {
            HttpSessionWrapper(S session, ServletContext servletContext) {
                super(session, servletContext);
            }

            public void invalidate() {
                super.invalidate();
                CafSessionRepositoryFilter.SessionRepositoryRequestWrapper.this.requestedSessionInvalidated = true;
                CafSessionRepositoryFilter.SessionRepositoryRequestWrapper.this.setCurrentSession(null);
                CafSessionRepositoryFilter.SessionRepositoryRequestWrapper.this.clearRequestedSessionCache();
                CafSessionRepositoryFilter.this.sessionRepository.deleteById(this.getId());
            }
        }
    }

    private final class SessionRepositoryResponseWrapper extends CafOnCommittedResponseWrapper {
        private final CafSessionRepositoryFilter<S>.SessionRepositoryRequestWrapper request;

        SessionRepositoryResponseWrapper(CafSessionRepositoryFilter<S>.SessionRepositoryRequestWrapper request, HttpServletResponse response) {
            super(response);
            if (request == null) {
                throw new IllegalArgumentException("request cannot be null");
            } else {
                this.request = request;
            }
        }

        protected void onResponseCommitted() {
            this.request.commitSession();
        }
    }
}
